﻿/*
Copyright (C) 2009  Tomasz Chrzanowski

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>
*/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;

namespace Com.TomCdc.ChangeDataCapture
{
    /// <summary>
    /// Represents the sql table of the source database.
    /// </summary>
    public class SourceTable
    {
        /// <summary>
        /// The name of a table sachema / owner.
        /// </summary>
        private string _tableSchema;

        /// <summary>
        /// The name of a table.
        /// </summary>
        private string _tableName;

        /// <summary>
        /// The table tracking state.
        /// </summary>
        private bool _isTracked;

        /// <summary>
        /// The collection of all columns which are part of the table.
        /// </summary>
        private Dictionary<string, SourceColumn> _columns;

        /// <summary>
        /// The list of coulumns included in a capture trigger of the source table, but not 
        /// existing in the that table.
        /// </summary>
        private CaseInsensitiveList _orphanTriggerCols;

        /// <summary>
        /// Gets the table identifier the object represents.
        /// </summary>
        /// <returns>The table identifier.</returns>
        public string TableIdentifier
        {
            get
            {
                return string.Format("{0}{1}{2}", _tableSchema, CommonHelper.SCHEMA_TABLE_SEPARATOR, _tableName);
            }
        }
        
        /// <summary>
        /// Gets the table schema.
        /// </summary>
        public string TableSchema
        {
            get
            {
                return _tableSchema;
            }
        }

        /// <summary>
        /// Gets the table name.
        /// </summary>
        public string TableName
        {
            get
            {
                return _tableName;
            }
        }

        /// <summary>
        /// Gets a value indicating wheather the table is currently tracked.
        /// </summary>
        /// <returns>True if the table is tracked, otherwise false.</returns>
        public bool IsTracked
        {
            get
            {
                return _isTracked;
            }
        }

        /// <summary>
        /// Gets the collection of columns being part of the table.
        /// </summary>
        /// <returns>The columns collection.</returns>
        public Dictionary<string, SourceColumn> Columns
        {
            get
            {
                return _columns;
            }
        }

        /// <summary>
        /// Gets value indicating whether the tracking trigger contains orphan column(s).
        /// Orphan column is when it doesn't exist in the table schema but is found if the trigger.
        /// </summary>
        public bool HasOrphanTriggerColumn
        {
            get
            {
                return _orphanTriggerCols.Count > 0 ? true : false;
            }
        }

        /// <summary>
        /// Gets the column which is the primary key or unique constaint.
        /// </summary>
        /// <returns>The primary key or unique constraint column, null if does not exist.</returns>
        public SourceColumn PrimaryKey
        {
            get
            {
                foreach (SourceColumn col in _columns.Values)
                {
                    if (col.IsPrimaryKey)
                    {
                        return col;
                    }
                }
                return null;
            }
        }

        /// <summary>
        /// Initializes a new instance of <see cref="SourceTable"/> with the table name and the state of
        /// tracking.
        /// </summary>
        /// <param name="tableName">The table name.</param>
        /// <param name="isTracked">The state of tracking.</param>
        public SourceTable(string tableSchema, string tableName, bool isTracked)
        {
            // validats input arguments 
            if (string.IsNullOrEmpty(tableName))
            {
                throw new ArgumentException("The tableName argument must not be empty or null.");
            }

            _tableName = tableName;

            _tableSchema = tableSchema;

            _isTracked = isTracked;

            // creates dictionary object to store columns collection
            _columns = new Dictionary<string, SourceColumn>(StringComparer.OrdinalIgnoreCase);

            _orphanTriggerCols = new CaseInsensitiveList();
        }

        /// <summary>
        /// Adds a column to the columns collection. A column name must be unique.
        /// </summary>
        /// <param name="newColumn">The column to be added.</param>
        /// <exception cref="CdcSchemaInstallerException">The table already contains provided column.</exception>
        public void AddColumn(SourceColumn newColumn)
        {
            if (newColumn == null)
            {
                throw new ArgumentNullException("newColumn");
            }

            if (_columns.ContainsKey(newColumn.ColumnName))
            {
                throw new CdcSchemaInstallerException("The table already contains column " +
                    newColumn.ColumnName);
            }

            bool duplicatedColumnPosition = false;
            foreach (SourceColumn srcCol in _columns.Values)
            {
                if (srcCol.OrdinalPosition == newColumn.OrdinalPosition)
                {
                    duplicatedColumnPosition = true;
                    break;
                }
            }

            if (duplicatedColumnPosition)
            {
                throw new CdcSchemaInstallerException("The newColumn must have unique ordinal " +
                    "position in the table.");
            }

            _columns.Add(newColumn.ColumnName, newColumn);
        }

        /// <summary>
        /// Adds an orphaned column to the orphaned columns collection. A column name must be unique.
        /// </summary>
        /// <param name="columnName">The column to be added.</param>
        /// <exception cref="CdcSchemaInstallerException">The table already contains provided 
        /// orphant column.</exception>
        public void AddOrphantColumn(string columnName)
        {
            // validates input column name
            if (string.IsNullOrEmpty(columnName))
            {
                throw new ArgumentException("The columnName argument must not be empty or null.");
            }

            if (_orphanTriggerCols.Contains(columnName))
            {
                throw new CdcSchemaInstallerException("The table already contains orphant column " +
                    columnName);
            }

            if (_columns.ContainsKey(columnName))
            {
                throw new CdcSchemaInstallerException("The table contains " + columnName +
                    " column in the list of existing columns.");
            }

            _orphanTriggerCols.Add(columnName);
        }

        /// <summary>
        /// Sets provided column name as primary key of the table.
        /// </summary>
        /// <param name="columnName">The table primary key.</param>
        /// <exception cref="CdcSchemaInstallerException">Cannot find column in the table columns 
        /// collection</exception>
        public void SetPrimaryKey(string columnName)
        {
            // validates input column name
            if (string.IsNullOrEmpty(columnName))
            {
                throw new ArgumentException("The columnName argument must not be empty or null.");
            }

            if (!_columns.ContainsKey(columnName))
            {
                throw new CdcSchemaInstallerException("The table does not contain column " +
                    columnName);
            }

            // validates if no primary key set yet, if already set, but to the column passed in the argument 
            // validation ends successfully
            foreach (SourceColumn col in _columns.Values)
            {
                if (col.IsPrimaryKey && !col.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase))
                {
                    throw new CdcSchemaInstallerException("The table contains already definied primary key, only " +
                        "one allowed per table.");
                }
            }

            _columns[columnName].IsPrimaryKey = true;
        }

        /// <summary>
        /// Gets the bit mask representing columns marked for tracking activation.
        /// </summary>
        /// <returns>The bit mask of columns to be included in tracking.</returns>
        public string GetToActivateBitmask()
        {
            //checks if any column to activate tracking
            if (!IsAnyToActivate())
            {
                throw new CdcSchemaInstallerException("No column set up for tracking activation.");
            }

            // prepares bit array used to collect tracked columns
            BitArray colBits = new BitArray(_columns.Count);

            // goes through all table columns and identifies those marked for tracking
            foreach (SourceColumn column in _columns.Values)
            {
                // marked for tracking
                if (column.ActivateTracking)
                {
                    // if bit array length smaller then column ordinar position increas length
                    if (colBits.Length < column.OrdinalPosition)
                    {
                        colBits.Length = column.OrdinalPosition;
                    }

                    // set columns bit to true
                    colBits.Set(column.OrdinalPosition - 1, true);
                }
            }

            return CommonHelper.ConvertToHexadecimal(colBits);
        }

        /// <summary>
        /// Gets the bit mask representing columns marked for tracking activation. It uses column ids from
        /// store database.
        /// </summary>
        /// <returns>The bit mask of columns to be included in tracking.</returns>
        public string GetToActivateBitmaskStoreIds()
        {
            //checks if any column to activate tracking
            if (!IsAnyToActivate())
            {
                throw new CdcSchemaInstallerException("No column set up for tracking activation.");
            }

            // prepares bit array used to collect tracked columns
            BitArray colBits = new BitArray(_columns.Count);

            // goes through all table columns and identifies those marked for tracking
            foreach (SourceColumn column in _columns.Values)
            {
                // marked for tracking
                if (column.ActivateTracking)
                {
                    // if bit array length smaller then column ordinar position increas length
                    if (colBits.Length < column.StoreSourceColumnOrdinal)
                    {
                        colBits.Length = column.StoreSourceColumnOrdinal;
                    }

                    // set columns bit to true
                    colBits.Set(column.StoreSourceColumnOrdinal - 1, true);
                }
            }

            return CommonHelper.ConvertToHexadecimal(colBits);
        }

        /// <summary>
        /// Gets the string array of a column names with tracking to activate.
        /// </summary>
        /// <returns>The array of column names.</returns>
        public string[] GetToActivateColNames()
        {
            //checks if any column to activate tracking
            if (!IsAnyToActivate())
            {
                throw new CdcSchemaInstallerException("No column set up for tracking activation.");
            }

            List<string> result = new List<string>(_columns.Count);
            foreach (SourceColumn column in _columns.Values)
            {
                // only includes not disabled and set for activation columns
                if (column.ActivateTracking && !column.IsDisabled)
                {
                    result.Add(column.ColumnName);
                }
            }
            return result.ToArray();
        }

        /// <summary>
        /// Gets the string array of sql data types of columns with tracking to activate.
        /// </summary>
        /// <returns>The array of data types.</returns>
        public string[] GetToActivateColTypes()
        {
            //checks if any column to activate tracking
            if (!IsAnyToActivate())
            {
                throw new CdcSchemaInstallerException("No column set up for tracking activation.");
            }

            List<string> result = new List<string>(_columns.Count);
            foreach (SourceColumn column in _columns.Values)
            {
                // only includes not disabled and set for activation columns data types
                if (column.ActivateTracking && !column.IsDisabled)
                {
                    // if the column length grater than 0, it means it's character columns and 
                    // length must be specified in definition
                    if (column.MaxLength > 0)
                    {
                        Debug.Assert(column.MaxLength <= 8000);
                        result.Add(column.DataType + "(" + column.MaxLength + ")");
                    }
                    else
                    {
                        result.Add(column.DataType);
                    }
                }
            }
            return result.ToArray();
        }

        /// <summary>
        /// Set activate tracking status of all not disabed columns.
        /// </summary>
        /// <param name="activate">The activation state to be set.</param>
        public void SetAllActivate(bool activate)
        {
            foreach (SourceColumn column in _columns.Values)
            {
                if (!column.IsDisabled)
                {
                    column.ActivateTracking = activate;
                }
            }
        }

        /// <summary>
        /// Checks if column name passed in argument is orphan.
        /// </summary>
        /// <param name="columnName">The name of the column to check.</param>
        /// <returns>True if the column is orphan, otherwise false.</returns>
        public bool IsColumnOrphan(string columnName)
        {
            // input value validation
            if (string.IsNullOrEmpty(columnName))
            {
                throw new ArgumentException("The columnName argument must not be empty or null.");
            }

            return _orphanTriggerCols.Contains(columnName);
        }

        /// <summary>
        /// Gets the value indicating whether any of table columns is set up for activation.
        /// </summary>
        /// <returns>True if any column set up for activation, otherwise false.</returns>
        public bool IsAnyToActivate()
        {
            bool isActivation = false;
            foreach (SourceColumn srcCol in _columns.Values)
            {
                if (srcCol.ActivateTracking)
                {
                    isActivation = true;
                    break;
                }
            }
            return isActivation;
        }

        /// <summary>
        /// Gets the value indicating whether source table tracking drop requested. 
        /// </summary>
        /// <returns>True when currently tracking active but no columns set up for activation.</returns>
        public bool IsDropTrackingRequested()
        {
            if (!_isTracked)
            {
                return false;
            }

            // if any column marked for activation returns false which means installation needed
            // otherwise false which means uninstallation needed
            return !IsAnyToActivate();
        }
    }
}
